#!/usr/bin/env python
#author: jiadong.zhuang@gmail.com
#date: 2009-08-06
import os, sys
import MySQLdb

class DataRow:
        def __init__(self, col_arr, row_data):
                for i in range( len(col_arr) ):
                        setattr( self, col_arr[i], row_data[i] )

class DataDescRow(DataRow):
	def __init__(self, row_data):
		_col_arr = ["Field","Type","Null","Key","Default","Extra"]
		DataRow.__init__(self, _col_arr, row_data)

class AyooTool:
	def dir_proj(cls):
		return os.path.abspath( os.path.join( os.getcwd(), '%s/../'%__file__[:__file__.rfind('/')] ) )
	dir_proj = classmethod(dir_proj)

class Table(object):
        def __init__(self):
                pass

        def count(cls, conn, s_count_field, s_table, s_condition):
                _sql = "SELECT COUNT(%s) FROM %s WHERE %s " % (s_count_field, s_table, s_condition)
                try:
                        cursor = conn.cursor()
                        cursor.execute(_sql)
                        tmp_list = cursor.fetchall()
                        if tmp_list:
                                return tmp_list[0][0]
                finally:
                        cursor.close()

        def query(cls, conn, s_field, s_table, s_condition):
                _col_arr = s_field.split(',')
                for i in range( len(_col_arr) ):
                        tmp = _col_arr[i].strip()
                        if ''==tmp : continue
                        _col_arr[i] = tmp
                _sql = "SELECT %s FROM %s WHERE %s " % (','.join(_col_arr), s_table, s_condition)
                try:
                        result = []
                        cursor = conn.cursor()
                        cursor.execute(_sql)
                        tmp_list = cursor.fetchall()
                        if tmp_list:
                                for r in tmp_list:
                                        result.append( DataRow( _col_arr, r) )
                        return result
                finally:
                        cursor.close()

	def desc(cls, conn, s_table):
		_sql = "DESC %s " % (s_table)
                try:
                        result = []
                        cursor = conn.cursor()
                        cursor.execute(_sql)
                        tmp_list = cursor.fetchall()
                        if tmp_list:
                                for r in tmp_list:
                                        result.append( DataDescRow( r ) )
                        return result
                finally:
                        cursor.close()

        #static-declaration.
        count=classmethod(count)
        query=classmethod(query)
        desc=classmethod(desc)

class SchemaTable:
	def __init__(self):
		self.clear()

	def clear(self):
		self.Database = ''
		self.Name = ''
		self.DumpPrefix = ''
		self.Sharding = ''
		self.Columns = {}
		self.Extra = {}

	def getName(self):
		return self.Name

	def getFixName(self):
		if self.Extra and self.Extra.has_key('dump_prefix_tb'):
			_tmp = self.Extra['dump_prefix_tb']
			if self.Name.find(_tmp)!=0 :
				return self.Name.capitalize()
			else:
				return self.Name[len(_tmp):].capitalize()
		else:
			return self.Name.capitalize()

class SchemaColumn:
	def __init__(self):
		self.clear()

	def clear(self):
		self.TbSchema = None
		self.Type = ''
		self.Name = ''
		self.DumpPrefix = ''
		self.Default = ''
		self.MaxLen = 0
		self.Comment = ''
		self.Extra = {}
	
	def getName(self):
		return self.Name

	def getFixName(self):
		if self.TbSchema and self.TbSchema.Extra.has_key('dump_prefix_col'):
			_tmp = self.TbSchema.Extra['dump_prefix_col']
			if self.Name.find(_tmp)!=0 :
				return self.Name
			else:
				return self.Name[len(_tmp):]
		else:
			return self.Name

	__T_PREFIX_MYSQL = {
	'unsigned':'u',
	}
	__T_DEFINE_MYSQL = {
	'int':'i32',
	'bigint':'i64',
	'varchar':'string',
	'text':'string',
	'float':'float',
	'double':'double',
	'char':'char',
	}
	def from_db(self, descData):
		_t_prefix = self.__T_PREFIX_MYSQL
		_t_define = self.__T_DEFINE_MYSQL
		_p = ''
		_t = ''
		_v = ''
		_max = 0
		self.Name = descData.Field
		if descData.Key and ''!= descData.Key:
			self.Extra['key'] = descData.Key

		for k,v in _t_prefix.items():
			if descData.Type.lower().find(k)>=0 :
				_p = v; break
		for k,v in _t_define.items():
			if descData.Type.lower().find(k)>=0 :
				_t = v; break
		self.Type = _p+_t
		if ''==_t :
			raise RuntimeError("type unknown:%s"%descData.Type)

		if descData.Default and 'string'==_t:
			self.Default = '"%s"' % descData.Default
		elif not descData.Default:
			self.Default = 'null'
		else:
			self.Default = descData.Default

		_idx1 = descData.Type.find('(')
		_idx2 = descData.Type.rfind(')')
		if 'string'==_t and _idx1>=0 and _idx2>=0:
			_max = int( descData.Type[ _idx1+1: _idx2 ] )

		self.MaxLen = _max
		if self.MaxLen > 0 :
			self.Extra['maxlen'] = str(self.MaxLen)
	

	__T_DEFINE_SWIFT = {
	'i32':'i32',
	'ui32':'ui32',
	'i64':'i64',
	'ui64':'ui64',
	'string':'string',
	'float':'float',
	'double':'double',
	'char':'char',
	}
	def __get_assign_pair(self, s_item):
		_kv = s_item.split('=')
		if len(_kv) < 2 :	
			raise RuntimeError("assign-item unknown:%s"%s_item)
		return ( _kv[0].strip(), _kv[1].strip() )

	def from_swift(self, line):
		_t_define = self.__T_DEFINE_SWIFT

		_line = line.strip()
		if _line.find('#')>0:
			self.Comment = _line[_line.find('#')+1:]
			_line = _line[:_line.find('#')]
		
		for k,v in _t_define.items():
			if _line.lower().find(k)>=0 :
				self.Type = v; break
		
		if ''==self.Type :
			raise RuntimeError("type unknown:%s"%_line)

		_line = _line[_line.find(' ')+1:]
		_items = _line.split(',')
		if len(_items)<1 :
			raise RuntimeError("declaration unknown:%s"%_line)
		(self.Name, self.Default) = self.__get_assign_pair( _items[0] )
		for _s_i in _items[1:]:
			(k,v)=self.__get_assign_pair( _s_i )
			self.Extra[k] = v
		
		if self.Extra.has_key('maxlen') :
			self.MaxLen = int(self.Extra['maxlen'])
		

	def to_swift(self):
		_line = "%s %s = %s" % (self.Type, self.Name, self.Default)
		if self.Extra.has_key('key'):
			_line += ', key=%s'%self.Extra['key']
		if self.MaxLen > 0 :
			_line += ", maxlen=%d"%self.MaxLen
		return _line
		


class ConfParser:
	def __init__(self):
		self.clear()
	
	def clear(self):
		self.__count = 0
		self.__cur_area = ''
		self.__cur_db = ''
		self.__cur_tb = ''
		self.table_name_list = []

	def iscomment(self, line):
		_line = line.strip()
		return  '#'==_line or ''==_line

	def feed(self, line):
		self.__count += 1
		if self.iscomment(line):
			return
		if '['==line[0] :
			self.__cur_area = line[ 1 : line.find(']') ]	
		elif 'server'==self.__cur_area and line.find('=')>0:
			_items = line.strip().split('=')
			setattr(self, _items[0], _items[1])
		elif 'schema'==self.__cur_area:
			if '_'==line[0] or line[0].isalpha() or line[0].isdigit() :
				self.__cur_db = line.strip()
			elif ''!=self.__cur_db and '\t'==line[0] :
				self.table_name_list.append( self.__cur_db + '.' + line.strip() )
			else:
				raise RuntimeError('syntax error at line: %d' % self.__count)

	def finish(self):
		pass


class SwiftParser:
	def __init__(self):
		self.clear()
	
	def clear(self):
		self.__count = 0
		self.__cur = None
		self.SchemaTables = {}

	def iscomment(self, line):
		_line = line.strip()
		return  '#'==_line or ''==_line
	
	def __table_done(self):
		if self.__cur and isinstance(self.__cur, SchemaTable) :
			self.SchemaTables[ self.__cur.DbName+'.'+self.__cur.Name ] = self.__cur

	def feed(self, line):
		self.__count += 1
		if self.iscomment(line):
			return
		#database.table
		if '_'==line[0] or line[0].isalpha() or line[0].isdigit() :
			_line = line.strip()
			if _line.find(':') > 0: 
				_line = _line[:_line.find(':')]
			_items = _line.split('.')
			if len(_items) != 2: 
				raise RuntimeError('syntax error at line: %d' % self.__count)

			#try to push parsed-table in to self.SchemaTables
			self.__table_done()

			self.__cur = SchemaTable()
			self.__cur.DbName = _items[0]
			self.__cur.Name = _items[1]

		#could be a column...
		elif '\t'==line[0] :
			_line = line.strip()
			if '$' == _line[0]:
				#table.Extra...
				_line = _line[1:]
				_k = _line[:_line.find('=')].strip()
				_v = _line[_line.find('=')+1:].strip()
				self.__cur.Extra[_k] = _v
			else:
				#column...
				_col = SchemaColumn()
				_col.from_swift( line )
				_col.TbSchema = self.__cur
				self.__cur.Columns[_col.Name] = _col
		else:
			raise RuntimeError('syntax error at line: %d' % self.__count)

	def finish(self):
		self.__table_done()	


class TypeManager:
	def __init__(self, lang):
		self.__def = {}

	def get(self, swift_type):
		if swift_type and self.__def.has_key( swift_type ) :
			return self.__def[ swift_type ]
		return 'UNKNOWN'


def gen_schema(input, output):
	p = ConfParser()
	try:
		for line in input.readlines():
			p.feed( line )
		p.finish()

	except RuntimeError, ex:
		print ex
		return
	conn = None
        try:
                conn = MySQLdb.connect(host=p.host, user=p.user, passwd=p.pswd, db='')
		for table_name in p.table_name_list:
			output.write(table_name+'\n')
			list = Table.desc(conn, table_name)
			if len(list)>0 :
				for i in range(len(list)):
					schm = SchemaColumn()
					schm.from_db( list[i] )
					output.write( "\t%s\n" % schm.to_swift() )
			output.write('\n')
        except Exception, e:
                print e
        if conn : conn.close()


def gen_code(input, out_dir):
	'''depending on mako-template-engine'''
	p = SwiftParser()
	try:
		for line in input.readlines():
			p.feed( line )
		p.finish()

	except RuntimeError, ex:
		print ex
		return
	if not out_dir or ''==out_dir:
		out_dir = './gen_code'

	try:
		os.mkdir(out_dir)
	except Exception,e:
		pass
	from mako.lookup import TemplateLookup
	from mako.template import Template
	_dir = '%s/tpl/'%AyooTool.dir_proj()
	_lookup = TemplateLookup(directories=[_dir] )
	_tpl_h = _lookup.get_template('dbi.h.tpl')
	_tpl_cpp = _lookup.get_template('dbi.cpp.tpl')
	for tb_fullname, tb_schema in p.SchemaTables.items():
		f_out = open('%s/dbi_%s.h'%(out_dir, tb_schema.Name), "w")
		f_out.write( _tpl_h.render(fullname=tb_fullname
					, classname = tb_schema.getFixName()
					, tablename = tb_schema.Name
					, columns = tb_schema.Columns
					, columns_key = _to_key_columns(tb_schema.Columns)
					, columns_nonkey = _to_nonkey_columns(tb_schema.Columns)
					)
		)

		f_out.close()
		print "############################"
		f_out = open('%s/dbi_%s.cpp'%(out_dir, tb_schema.Name), "w")
		f_out.write( _tpl_cpp.render(fullname = tb_fullname
					, classname = tb_schema.getFixName()
					, tablename = tb_schema.Name
					, columns = tb_schema.Columns
					, columns_key = _to_key_columns(tb_schema.Columns)
					, columns_nonkey = _to_nonkey_columns(tb_schema.Columns)
					)
		)
		f_out.close()


def _to_key_columns(cols):
	res = {} 
	for col_name, col in cols.items():
		if col.Extra.has_key('key') : res[col_name] = col
	return res

def _to_nonkey_columns(cols):
	res = {} 
	for col_name, col in cols.items():
		if col.Extra.has_key('key') and ""!=col.Extra['key'] : continue
		else: 
			res[col_name] = col
	return res


def main():	
	from optparse import OptionParser
	p = OptionParser(version='%prog 0.22')
	p.add_option("-t", "--type", dest="type", help="schema|gen")
	p.add_option("-i", "--input", dest="input", help="input file")
	p.add_option("-o", "--output", dest="output", help="output file")
	(opts, args) = p.parse_args()
	_m = vars(opts)
	
	if _m['type'] is None:
		print "option -t(--type) is needed."
		sys.exit()
	
	if 'schema'==_m['type']:
		_in = sys.stdin
		_out = sys.stdout
		if _m['input'] is not None :
			_in = open(_m['input'])
		gen_schema(_in, _out)
		_in.close()
		_out.close()
	elif 'code'==_m['type']:
		_in = sys.stdin
		if _m['input'] is not None :
			_in = open(_m['input'])
		if _m['output'] is None :
			_m['output'] = './gensrc_cpp'
		gen_code(_in, _m['output'])
		_in.close()
	else:
		print "unknown swift-type:%s." % _m['type']
		sys.exit()
		
	

